﻿//	MIT License
//
//	Copyright(c) 2017 Thomas Monkman
//
//	Permission is hereby granted, free of charge, to any person obtaining a copy
//	of this software and associated documentation files(the "Software"), to deal
//	in the Software without restriction, including without limitation the rights
//	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//	copies of the Software, and to permit persons to whom the Software is
//	furnished to do so, subject to the following conditions :
//
//	The above copyright notice and this permission notice shall be included in all
//	copies or substantial portions of the Software.
//
//	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
//	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//	SOFTWARE.



#ifndef AMO_FILEWATCH_T_25F0599B_1189_484A_9EEF_77B9BB5E026F_HPP__
#define AMO_FILEWATCH_T_25F0599B_1189_484A_9EEF_77B9BB5E026F_HPP__









#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
#include <tchar.h>
#include <Pathcch.h>
#include <shlwapi.h>
#endif // WIN32

#if __unix__
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <unistd.h>
#endif // __unix__

#include <functional>
#include <atomic>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <utility>
#include <vector>
#include <array>
#include <map>
#include <system_error>
#include <string>
#include <algorithm>
#include <type_traits>
#include <future>
#include <regex>


namespace amo {
    enum class Event {
        added,
        removed,
        modified,
        renamed_old,
        renamed_new
    };
    
    /**
    * \class FileWatch
    *
    * \brief Watches a folder or file, and will notify of changes via function callback.
    *
    * \author Thomas Monkman
    *
    */
    template<class T>
    class filewatch_t {
        typedef std::basic_string<typename T::value_type, std::char_traits<typename T::value_type>>
                UnderpinningString;
        typedef std::basic_regex<typename T::value_type, std::regex_traits<typename T::value_type>>
                UnderpinningRegex;
                
    public:
    
        filewatch_t(T path, UnderpinningRegex pattern,
                    std::function<void(const T& file, const Event event_type)> callback) :
            _path(path),
            _pattern(pattern),
            _callback(callback),
            _directory(get_directory(path)) {
            init();
        }
        
#if defined _WIN32 && (defined UNICODE || defined _UNICODE)
        filewatch_t(T path, std::function<void(const T& file, const Event event_type)>
                    callback) :
            filewatch_t<T>(path, UnderpinningRegex((UnderpinningString)amo::string(".*", false)),
                           callback) {}
#else // _WIN32 && (UNICODE || _UNICODE)
        filewatch_t(T path, std::function<void(const T& file, const Event event_type)>
                    callback) :
            filewatch_t<T>(path, UnderpinningRegex(".*"), callback) {}
#endif
                           
        ~filewatch_t() {
            destroy();
        }
        
        filewatch_t(const filewatch_t<T>& other) : filewatch_t<T>(other._path,
                    other._callback) {}
                    
        filewatch_t<T>& operator=(const filewatch_t<T>& other) {
            if (this == &other) {
                return *this;
            }
            
            destroy();
            _path = other._path;
            _callback = other._callback;
            _directory = get_directory(other._path);
            init();
            return *this;
        }
        
        // Const memeber varibles don't let me implent moves nicely, if moves are really wanted std::unique_ptr should be used and move that.
        filewatch_t<T>(filewatch_t<T>&&) = delete;
        filewatch_t<T>& operator=(filewatch_t<T>&&) & = delete;
        
    private:
        struct PathParts {
            PathParts(T directory, T filename) : directory(directory), filename(filename) {}
            T directory;
            T filename;
        };
        const T _path;
        
        UnderpinningRegex _pattern;
        
        static constexpr std::size_t _buffer_size = { 1024 * 256 };
        
        // only used if watch a single file
        bool _watching_single_file = { false };
        T _filename;
        
        std::atomic<bool> _destory = { false };
        std::function<void(const T& file, const Event event_type)> _callback;
        
        std::thread _watch_thread;
        
        std::condition_variable _cv;
        std::mutex _callback_mutex;
        std::vector<std::pair<T, Event>> _callback_information;
        std::thread _callback_thread;
        
        std::promise<void> _running;
#ifdef _WIN32
        HANDLE _directory = { nullptr };
        HANDLE _close_event = { nullptr };
        
        const DWORD _listen_filters =
            FILE_NOTIFY_CHANGE_SECURITY |
            FILE_NOTIFY_CHANGE_CREATION |
            FILE_NOTIFY_CHANGE_LAST_ACCESS |
            FILE_NOTIFY_CHANGE_LAST_WRITE |
            FILE_NOTIFY_CHANGE_SIZE |
            FILE_NOTIFY_CHANGE_ATTRIBUTES |
            FILE_NOTIFY_CHANGE_DIR_NAME |
            FILE_NOTIFY_CHANGE_FILE_NAME;
            
        const std::map<DWORD, Event> _event_type_mapping = {
            { FILE_ACTION_ADDED, Event::added },
            { FILE_ACTION_REMOVED, Event::removed },
            { FILE_ACTION_MODIFIED, Event::modified },
            { FILE_ACTION_RENAMED_OLD_NAME, Event::renamed_old },
            { FILE_ACTION_RENAMED_NEW_NAME, Event::renamed_new }
        };
#endif // WIN32
        
#if __unix__
        struct FolderInfo {
            int folder;
            int watch;
        };
        
        FolderInfo  _directory;
        
        const std::uint32_t _listen_filters = IN_MODIFY | IN_CREATE | IN_DELETE;
        
        const static std::size_t event_size = (sizeof(struct inotify_event));
#endif // __unix__
        
        void init() {
#ifdef _WIN32
            _close_event = CreateEvent(NULL, TRUE, FALSE, NULL);
            
            if (!_close_event) {
                throw std::system_error(GetLastError(), std::system_category());
            }
            
#endif // WIN32
            _callback_thread = std::move(std::thread([this]() {
                try {
                    callback_thread();
                } catch (...) {
                    try {
                        _running.set_exception(std::current_exception());
                    } catch (...) {} // set_exception() may throw too
                }
            }));
            _watch_thread = std::move(std::thread([this]() {
                try {
                    monitor_directory();
                } catch (...) {
                    try {
                        _running.set_exception(std::current_exception());
                    } catch (...) {} // set_exception() may throw too
                }
            }));
            
            std::future<void> future = _running.get_future();
            future.get(); //block until the monitor_directory is up and running
        }
        
        void destroy() {
            _destory = true;
            _running = std::promise<void>();
#ifdef _WIN32
            SetEvent(_close_event);
#elif __unix__
            inotify_rm_watch(_directory.folder, _directory.watch);
#endif // __unix__
            _cv.notify_all();
            _watch_thread.join();
            _callback_thread.join();
#ifdef _WIN32
            CloseHandle(_directory);
#elif __unix__
            close(_directory.folder);
#endif // __unix__
        }
        
        const PathParts split_directory_and_file(const T& path) const {
            const auto predict = [](typename T::value_type character) {
#ifdef _WIN32
                return character == _T('\\') || character == _T('/');
#elif __unix__
                return character == '/';
#endif // __unix__
            };
#ifdef _WIN32
#define _UNICODE
            const UnderpinningString this_directory = amo::string("./", false);
#elif __unix__
            const UnderpinningString this_directory = "./";
#endif // __unix__
            
            const auto pivot = std::find_if(path.rbegin(), path.rend(), predict).base();
            //if the path is something like "test.txt" there will be no directoy part, however we still need one, so insert './'
            const T directory = [&]() {
                const auto extracted_directory = UnderpinningString(path.begin(), pivot);
                return (extracted_directory.size() > 0) ? extracted_directory : this_directory;
            }
            ();
            const T filename = UnderpinningString(pivot, path.end());
            return PathParts(directory, filename);
        }
        
        bool pass_filter(const UnderpinningString& file_path) {
            if (_watching_single_file) {
                const UnderpinningString extracted_filename = { split_directory_and_file(file_path).filename };
                //if we are watching a single file, only that file should trigger action
                return extracted_filename == _filename;
            }
            
            return std::regex_match(file_path, _pattern);
        }
        
#ifdef _WIN32
        HANDLE get_directory(const T& path) {
            auto file_info = GetFileAttributes(amo::string(
                                                   path.c_str(), false).to_unicode().c_str());
                                                   
            if (file_info == INVALID_FILE_ATTRIBUTES) {
                throw std::system_error(GetLastError(), std::system_category());
            }
            
            _watching_single_file = (file_info & FILE_ATTRIBUTE_DIRECTORY) == false;
            
            const T watch_path = [this, &path]() {
                if (_watching_single_file) {
                    const auto parsed_path = split_directory_and_file(path);
                    _filename = parsed_path.filename;
                    return parsed_path.directory;
                } else {
                    return path;
                }
            }
            ();
            
            HANDLE directory = ::CreateFile(
                                   amo::string(
                                       watch_path.c_str(), false).to_unicode().c_str(),           // pointer to the file name
                                   FILE_LIST_DIRECTORY,    // access (read/write) mode
                                   FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // share mode
                                   NULL, // security descriptor
                                   OPEN_EXISTING,         // how to create
                                   FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, // file attributes
                                   NULL);                 // file with attributes to copy
                                   
            if (directory == INVALID_HANDLE_VALUE) {
                throw std::system_error(GetLastError(), std::system_category());
            }
            
            return directory;
        }
        void monitor_directory() {
            std::vector<BYTE> buffer(_buffer_size);
            DWORD bytes_returned = 0;
            OVERLAPPED overlapped_buffer{ 0 };
            
            overlapped_buffer.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
            
            if (!overlapped_buffer.hEvent) {
                std::cerr << "Error creating monitor event" << std::endl;
            }
            
            std::array<HANDLE, 2> handles{ overlapped_buffer.hEvent, _close_event };
            
            auto async_pending = false;
            _running.set_value();
            
            do {
                std::vector<std::pair<T, Event>> parsed_information;
                memset(buffer.data(), 0, buffer.size());
                ReadDirectoryChangesW(
                    _directory,
                    buffer.data(), buffer.size(),
                    TRUE,
                    _listen_filters,
                    &bytes_returned,
                    &overlapped_buffer, NULL);
                    
                async_pending = true;
                
                switch (WaitForMultipleObjects(2, handles.data(), FALSE, INFINITE)) {
                case WAIT_OBJECT_0: {
                    if (!GetOverlappedResult(_directory, &overlapped_buffer, &bytes_returned,
                                             TRUE)) {
                        throw std::system_error(GetLastError(), std::system_category());
                    }
                    
                    async_pending = false;
                    
                    if (bytes_returned == 0) {
                        break;
                    }
                    
                    FILE_NOTIFY_INFORMATION *file_information =
                        reinterpret_cast<FILE_NOTIFY_INFORMATION*>(&buffer[0]);
                        
                    do {
                        /*typename UnderpinningString::value_type str[1000] = { 0 };
                        wmemcpy(str, file_information->FileName, file_information->FileNameLength / 2);*/
                        std::wstring str(file_information->FileName,
                                         file_information->FileNameLength /
                                         2);
                        UnderpinningString changed_file = amo::string(
                                                              str);  // { file_information->FileName, file_information->FileNameLength / 2 };
                                                              
                        if (pass_filter(changed_file)) {
                            parsed_information.emplace_back(T{ changed_file }, _event_type_mapping.at(
                                                                file_information->Action));
                        }
                        
                        if (file_information->NextEntryOffset == 0) {
                            break;
                        }
                        
                        file_information = reinterpret_cast<FILE_NOTIFY_INFORMATION*>
                                           (reinterpret_cast<BYTE*>(file_information) + file_information->NextEntryOffset);
                    } while (true);
                    
                    break;
                }
                
                case WAIT_OBJECT_0 + 1:
                    // quit
                    break;
                    
                case WAIT_FAILED:
                    break;
                }
                
                //dispatch callbacks
                {
                    std::lock_guard<std::mutex> lock(_callback_mutex);
                    _callback_information.insert(_callback_information.end(),
                                                 parsed_information.begin(), parsed_information.end());
                }
                _cv.notify_all();
            } while (_destory == false);
            
            if (async_pending) {
                //clean up running async io
                CancelIo(_directory);
                GetOverlappedResult(_directory, &overlapped_buffer, &bytes_returned, TRUE);
            }
        }
#endif // WIN32
        
#if __unix__
        
        bool is_file(const T& path) const {
            struct stat statbuf = {};
            
            if (stat(path.c_str(), &statbuf) != 0) {
                throw std::system_error(errno, std::system_category());
            }
            
            return S_ISREG(statbuf.st_mode);
        }
        
        FolderInfo get_directory(const T& path) {
            const auto folder = inotify_init();
            
            if (folder < 0) {
                throw std::system_error(errno, std::system_category());
            }
            
            const auto listen_filters = _listen_filters;
            
            _watching_single_file = is_file(path);
            
            const T watch_path = [this, &path]() {
                if (_watching_single_file) {
                    const auto parsed_path = split_directory_and_file(path);
                    _filename = parsed_path.filename;
                    return parsed_path.directory;
                } else {
                    return path;
                }
            }
            ();
            
            const auto watch = inotify_add_watch(folder, watch_path.c_str(),
                                                 IN_MODIFY | IN_CREATE | IN_DELETE);
                                                 
            if (watch < 0) {
                throw std::system_error(errno, std::system_category());
            }
            
            return { folder, watch };
        }
        
        void monitor_directory() {
            std::vector<char> buffer(_buffer_size);
            
            _running.set_value();
            
            while (_destory == false) {
                const auto length = read(_directory.folder, static_cast<void*>(buffer.data()),
                                         buffer.size());
                                         
                if (length > 0) {
                    int i = 0;
                    std::vector<std::pair<T, Event>> parsed_information;
                    
                    while (i < length) {
                        struct inotify_event *event = reinterpret_cast<struct inotify_event *>
                                                          (&buffer[i]); // NOLINT
                                                          
                        if (event->len) {
                            const UnderpinningString changed_file{ event->name };
                            
                            if (pass_filter(changed_file)) {
                                if (event->mask & IN_CREATE) {
                                    parsed_information.emplace_back(T{ changed_file }, Event::added);
                                } else if (event->mask & IN_DELETE) {
                                    parsed_information.emplace_back(T{ changed_file }, Event::removed);
                                } else if (event->mask & IN_MODIFY) {
                                    parsed_information.emplace_back(T{ changed_file }, Event::modified);
                                }
                            }
                        }
                        
                        i += event_size + event->len;
                    }
                    
                    //dispatch callbacks
                    {
                        std::lock_guard<std::mutex> lock(_callback_mutex);
                        _callback_information.insert(_callback_information.end(),
                                                     parsed_information.begin(), parsed_information.end());
                    }
                    _cv.notify_all();
                }
            }
        }
#endif // __unix__
        
        void callback_thread() {
            while (_destory == false) {
                std::unique_lock<std::mutex> lock(_callback_mutex);
                
                if (_callback_information.empty() && _destory == false) {
                    _cv.wait(lock, [this] { return _callback_information.size() > 0 || _destory; });
                }
                
                decltype(_callback_information) callback_information = {};
                std::swap(callback_information, _callback_information);
                lock.unlock();
                
                for (const auto& file : callback_information) {
                    if (_callback) {
                        try {
                            _callback(file.first, file.second);
                        } catch (const std::exception&) {
                        }
                    }
                }
            }
        }
    };
    
    
}



#endif //AMO_FILEWATCH_T_25F0599B_1189_484A_9EEF_77B9BB5E026F_HPP__